2019/05/16

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

Go は最近のプログラミング言語にしては珍しくポインタを扱えるプログラミング言語。とはいってもC言語よりも簡単で、オブジェクトの初期化やメソッドの定義以外の場所ではおおよそポインタを使っている様には見えない。メソッドやフィールドへのアクセスも . で出来るし Duck Type によりインタフェースを満たしていれば実体であろうとポインタであろうとそれほど意識する必要はない。ところがこの便利さに乗っかってしまうと思わぬ所で足をすくわれてしまう。

package main

type foo struct {
    v int
}

func (f foo) add(v int) {
    f.v = v
}

func main() {
    var a foo

    a.add(3)

    println(a.v)
}

このコードは 0 が表示される。メソッドを呼び出す際にはレシーバのオブジェクトを得る必要があるが、foo は実体なのでコピーが生成される。例えばレシーバ f が引数であったと考えると理解しやすくなる。

package main

type foo struct {
    v int
}

func add(f foo, v int) {
    f.v += v
}

func main() {
    var a foo

    add(a, 3)

    println(a.v)
}

メソッドとレシーバの関係は、実は「単なる関数と第一引数」と考えると分かりやすい。特にC言語でオブジェクト指向をやる様な人達は訓練されているので、Go をやる上でもこの辺りの動作を意識せず扱えているのかもしれない。

実体のレシーバにメソッドを生やすメリットが無い訳ではない。例えばオブジェクトの値を明示的に変更させたくない場合がそれで、そういった設計にしたい場合は戻り値を使う。

package main

type foo struct {
    v int
}

func (f foo) add(v int) foo {
    f.v += v
    return f
}

func main() {
    var a foo

    a = a.add(3)

    println(a.v)
}

これとは別に、ポインタと実体をまぜて考えてしまうと失敗してしまう事もある。

この例のポイントは「for range の反復変数はループ毎に新しいコピーが作成されない」という事。つまり上書きになる。プログラマは一見このループが実行されると以下の様になると考えてしまう。

= append(b, c[0].f())
= append(b, c[1].f())

だが実際はこう。

var i a
= c[0]
= append(b, i.f())
= c[1]
= append(b, i.f())

The Go Playground

slice の b は一見、c の情報が詰め込まれている様に見えるが、実際は i の情報が詰め込まれている。なので2回目の i への代入時に「i が持っていたレシーバの情報 c[0]c[1] 上書きされてしてしまう事になり、結果 a, b ではなく b, b が表示される。Method values という仕組みは参考として見ておくと良い。i への代入時点でレシーバの情報が デリファレンス されコピーされた状態で代入されているのがポイント、f() の呼び出し時の話ではない。

package main

import (
    "fmt"
)

type a struct {
    N string
}

func (s *a) f() func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

func main() {
    b := []func(){}
    c := []a{{"a"}, {"b"}}

    var i a

    i = c[0]
    b = append(b, i.f())

    b[0]()
    i = c[1]
    b[0]() // この時点で既に b になる
    b = append(b, i.f())
}

まとめ

混ぜるな危険

Posted at by